Racket 编程入门:1、RACKEET 基础

先来介绍一下 Racket 中的一些基本概念。在这一章中,介绍本书中会用到的一些基本数据类型。你需要特别注意对列表的讨论,它是 Racket 大部分功能的基础。还介绍如何给变量赋值,以及操作字符串的各种方方法,在这过程中,还会初探向量和结构。本章最后讨论如何产生格式化的输出。
原子数据
原子数据是任何编程语言的基本构建块,Racket 也不例外。原子数据指通常是不可分割的实体的基本数据类型;也就是像 123 这样的数字,像 "hello there" 这样的字符串,以及 pi 这样的标识符。数字和字符串对自身进行求值;如果绑定,标识符对其关联的值进行求值:
> 123
123
> "hello there"
"hello there"
> pi
3.141592653589793
对未绑定的标识符进行求值,会导致错误。为了防止未绑定的标识符被求值,可以在它前面加上撇号:
> alpha
. . alpha: undefined;
cannot reference an identifier before its definition
> 'alpha
'alpha
可以使用列表将原子数据组织在一起,下面介绍这一点。
列表
在 Rakcet 中,列表是主要的非原子数据结构(也就是说,不是数字、字符串等)。Racket 非常依赖列表,因为它是 Lisp(LISt Processing 的缩写)的衍生物。在深入讨论细节之前,先看看一些简单的列表性样本。
第一次看列表
以下是如何列出一些数字的列表:
> (list 1 2 3)
注意语法。列表通常以左圆括号(开始,后跟一列以空格分隔的项,并以右圆括号)结束。列表中的第一项通常是一个标识符,指示如何对列表求值。
列表还可以包含其他列表。
> (list 1 (list "two" "three") 4 5)
它打印
'(1 ("two" "three") 4 5)
虽然 list 和 quote 似乎是两种构建列表的相同方法,但它们之间有一个重要的区别。下面的序列说明它们之间的区别。
> (quote (3 1 3 pi))
'(3 1 4 pi)
> (list 3 1 4 pi)
'(3 1 4 3.141592653589793)
请注意,quote 返回的列表与输入时完全一样,但使用 list 时,会对标识符 pi 求值,并将值替换到它的位置上。通常,在非引号列表中,所有标识符都被求值并替换为它们关联的值。关键字 quote 在宏和符号表达式求值中扮演着重要的角色,这是在本文中不涉及的高级主题。
一种对 Lisp 语言家族的批评是括号太多。为了减轻这种情况,Racket 允许用方括号或花括号代替。例如,将最后一个表达式写成
> '(1 ["two" "three"] 4 5)
或
> '(1 {"two"} {"three"} 4 5)
S 表达式
列表是 S 表达式的一种特殊情况。S 表达式(或符号表达式)的定义分为两种情形:
情形 1 S 表达式是一个原子
情形 2 S 表达式 (x . y),其中 x 和 y 是其他的 S 表达式。
(x . y) 通常成为对。这是一种指定 cons 单元格的特殊语法形式,稍后详细介绍。
我们看看能不能构造几个 S 表达式的例子。啊,那 1 呢?是的,它是一个原子,所有它满足情形 1。那 "spud" 呢?是的,字符串是原子,因此 "spud" 也是一个 S 表达式。可以把它们结合起来,生成另一个 S 表达式。(1 . "spud"),它满足情形 2。由于 (1 . "spud") 是一个 S 表达式,所以情形 2 允许生成另一个 S 表达式为 ((1 . "spud") . (1 . "spud"))。由此可以看出,实际上 S 表达式是树状结构,如下图所示。从技术上讲,S 表达式形成一颗二叉树,其中非叶子节点正好有两个子节点。

在图中,方框是代表原子的叶子节点,圆圈节点代表对。在下一节中将看到如何用 S 表达式来构造列表。
列表结构
如上所述,列表是 S 表达式的一种特殊情形。不同的是,在列表中,如果遵循每对元素中最右边的元素是一个对,最后的节点是一个特殊的原子节点,成为 nil。下图说明列表 '(1 2 3) 作为 S 表达式,其内部样子像是 (1 . (2 . (3 . nil)))。

我们已经将树扁平化,使其更像一个列表。还扩展每个对节点(又称 cons 单元格),以显示它由两个单元格组成,每个单元格都包含一个指向另一个节点的指针。由于历史原因,这些指针的单元分别称为 car 和 cdr(Lisp 早期版本中使用的计算机寄存器的名称)。可以看到,列表中最后一个 cdr 单元指向 nil。在 Racket 中,nil 用一个空列表来表示:'() 或 null。
cons 单元格可用 cons 函数直接创建。注意,cons 函数不一定会创建一个列表。例如
> (cons 1 2)
'(1 . 2)
产生一个对而不是一个列表。然而,如果使用一个空列表作为第二个 S 表达式
> (cons 1 '())
'(1)
产生一个只有一个元素的列表。
Racket 提供几个函数来检测某个东西使列表还是对。注意在 Racket 中 #t 表示 true,#f 表示 false:
> (pair? (cons 1 2))
#t
> (list? (cons 1 2))
#f
> (pair? (cons 1 '()))
#t
> (list? (cons 1 '()))
#t
由此可以看出,一个列表总是一对,但反过来并不总是对的:一对并不总是一个列表。
通常,cons 用于在列表开头添加一个原子值,如下所示:
> (cons 1 '(2 3))
'(1 2 3)
Racket 提供特殊的,以访问 cons 单元格的组件。car 函数返回 car 指针所指向的项目,相应地 cdr 函数返回 cdr 指针所指向的项目。在 Racket 中,函数 first 和 rest 类似于 car 和 cdr,但它们不是这些函数的别名,因为它们只与列表一起工作。下面给出了几个例子。
> (car '(1 ("two" "three") 4 5))
1
> (first '(1 ("two" "three") 4 5))
1
> (cdr '(1 ("two" "three") 4 5))
'(("two" "three") 4 5)
> (rest '(1 ("two" "three") 4 5))
'(("two" "three") 4 5)
列表元素也可以通过函数 second、third、...、tenth 访问
> (first '(1 2 3 4))
1
> (second '(1 2 3 4))
2
> (third '(1 2 3 4))
3
最后,可以使用 list-ref 提取任意位置的值。
> (list-ref '(a b c) 0)
'a
> (list-ref '(a b c) 1)
'b
list-ref 函数接受一个列表和你想要的值的索引,列表在前面。请注意,Racket 使用从零开始的索引,这意味着对于任何值序列,第一个值的索引是 0,第二个值的索引是 1,以此类推。
一些有用的列表函数
让我们快速浏览一些有用的列表函数
长度
要获得一个列表的长度,可以使用 length 函数,就像这样:
> (length '(1 2 3 4 5))
5
反转
如果需要反转列表中的元素,可以使用 reverse 函数。
> (reverse '(1 2 3 4 5)) ; reverse elements of a list
'(5 4 3 2 1)
排序
sort 函数对列表进行排序。可以传入 < 来按升序排序列表:
> (sort '(1 3 6 5 7 9 2 4 8) <)
'(1 2 3 4 5 6 7 8 9)
或者,如果传入 >,它按降序排列:
> (sort '(1 3 6 5 7 9 2 4 8))
'(9 8 7 6 5 4 3 2 1)
合并
要合并两个列表,可以使用 append 函数:
> (append '(1 2 3) '(4 5 6))
'(1 2 3 4 5 6)
append 函数可以接受两个以上的列表:
> (append '(1 2) '(3 4) '(5 6))
'(1 2 3 4 5 6)
值域
range 函数根据某些规范创建一个数字列表。可以传递一个起始值和一个结束值,以及一个要递增的步长:
> (range 0 10 2)
'(0 2 4 6 8)
或者,如果只传递一个结束值,它从 0 开始,步长为 1:
> (range 10)
'(0 1 2 3 4 5 6 7 8 9)
造表
另一种制作列表的方法是使用 make-list 函数:
> (make-list 10 'me)
'(me me me me me me me me me me)
如您所见,make-list 接受一个数字和一个值,并使包含该值的列表重复该次数。
空表?
要检测列表是否为空,可以使用 null? 函数:
> (null? '()); test for empty list
#t
> (null? '(1 2 3))
#f
索引
如果需要在列表中搜索值的索引,可以使用 index-of。它将返回值的索引,如果存在,则返回值的索引:
> (index-of '(8 7 1 9 5 2) 9)
3
如果没有,则返回 #f:
> (index-of '(8 7 1 9 5 2) 10)
#f
成员
搜索列表的另一种方法使使用 member,它检测列表是否包含特定元素。如果没有,则返回符号 #f;如果匹配,则返回从匹配元素的第一个实例开始的列表尾部。
> (member 7 '(9 3 5 (6 2) 5 1 4))
#f
> (member 5 '(9 3 5 (6 2) 5 1 4))
'(5 (6 2) 5 1 4)
> (member 6 '(9 3 5 (6 2) 5 1 4))
#f
注意,在最后一个实例中,即使 6 是所搜索列表的子列表的成员,member 函数仍然返回 false。然而,以下是可行的。
> (member '(6 2) '(9 3 5 (6 2) 5 14))
'((6 2) 5 1 4)
稍后将看到,在函数式编程中,经常需要确定一个项是否包含在列表中。member 函数不仅查找项(如果存在的话),而且返回实际值,以便在进一步的计算中使用。
在本文的其余部分,将更多的讨论列表。